using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Xml;

using Microsoft.TeamFoundation.VersionControl.Client;
using Microsoft.TeamFoundation.WorkItemTracking.Client;

using TfsSpam.Differ.Logging;
using TfsSpam.Utilities;

namespace TfsSpam.Differ
{
    /// <summary>
    /// The ChangesetDiff class is used to generate a XML document containing
    /// information about the actions taken in a changeset.
    /// </summary>
    public class ChangesetDiff
    {
        #region -- Private class constants
        /// <summary>
        /// The maximum number of characters from a changeset comment
        /// that gets included in the title for the changeset analysis.
        /// </summary>
        private const int MaxCommentLength = 80;

        /// <summary>
        /// The CDATA section terminator.
        /// </summary>
        private const string CDataTerminator = "]]>";
        #endregion

        #region -- Private class members
        /// <summary>
        /// The version control services on the TFS server
        /// the changeset is related to.
        /// </summary>
        private readonly VersionControlService _versionControl;

        /// <summary>
        /// The changeset that we'll generate the difference
        /// information for.
        /// </summary>
        private readonly Changeset _changeset;

        /// <summary>
        /// The dictionary used to store the information about
        /// which file types should have detail information included
        /// in the generated XML document.
        /// </summary>
        private Dictionary<string, bool> _fileDetailExtensions = new Dictionary<string, bool>( StringComparer.CurrentCultureIgnoreCase );

        /// <summary>
        /// The list used to store the information about
        /// which prefixes (e.g. TODO:)on added lines should be 
        /// included in the code highlight summary.
        /// </summary>
        private List<string> _highlightPrefixes = new List<string>();

        /// <summary>
        /// The logging object used for the changeset being processed.
        /// </summary>
        private SpamLogger _logger;
        #endregion

        #region -- Constructor
        /// <summary>
        /// Standard constructor for the class, initializes the object by connecting
        /// to the specified TFS server and project and retrieves the specified changeset.
        /// </summary>
        /// <param name="versionControl">The version control service used to download the items in the changeset.</param>
        /// <param name="changeset">The changeset that should be processed.</param>
        /// <param name="logger">The logging object that should be used for this changeset.</param>
        internal ChangesetDiff( VersionControlService versionControl, Changeset changeset, SpamLogger logger )
        {
            _versionControl = versionControl;
            _changeset = changeset;
            _logger = logger;
        }
        #endregion

        #region -- Public methods
        /// <summary>
        /// Writes an XML document that represents the actions taken in the
        /// changeset to the provided stream.
        /// </summary>
        /// <param name="targetStream">The <see cref="Stream"/> the changeset actions should be written to.</param>
        public void WriteChangesetDocument( Stream targetStream )
        {
            _logger.Verbose( "ChangesetDiff.WriteChangesetDocument" );

            using ( XmlTextWriter xmlWriter = new XmlTextWriter( targetStream, System.Text.Encoding.UTF8 ) )
            {
                xmlWriter.WriteStartElement( "checkin" );

                _logger.Verbose( "Adding basic changeset information" );
                AddBasicChangesetInformation( xmlWriter );
                _logger.Verbose( "Adding detailed item information" );
                AddItemInformation( xmlWriter );

                xmlWriter.WriteEndElement();
                _logger.Verbose( "Changeset analysis XML document complete" );
            }
        }

        /// <summary>
        /// Adds a file extension to the list of file extensions that define
        /// the file types that should have detail information included in the
        /// generated XML document.
        /// </summary>
        /// <remarks>
        /// File extensions should be specified with the leading period (e.g. .cpp).
        /// </remarks>
        /// <param name="fileExtension">
        /// The file extension for the file type that should have detail information included.
        /// </param>
        public void AddFileDetailExtension( string fileExtension )
        {
            if ( !_fileDetailExtensions.ContainsKey( fileExtension ) )
            {
                _logger.Verbose( "Adding the file extension '{0}' as a detail file type", fileExtension );
                _fileDetailExtensions.Add( fileExtension, true );
            }
        }

        /// <summary>
        /// Adds a code highlight prefix to the list of prefixes that will be
        /// processed for all added lines in the files that are part of the 
        /// changeset being processed.
        /// </summary>
        /// <param name="prefix">The prefix (e.g. TODO:) to add.</param>
        public void AddHighlightPrefix( string prefix )
        {
            _logger.Verbose( "Adding the highlight prefix '{0}'", prefix );
            _highlightPrefixes.Add( prefix );
        }
        #endregion

        #region -- Public properties
        /// <summary>
        /// The number of context lines that should be included
        /// when comparing two versions of a file.
        /// </summary>
        [DefaultValue( FileDiffer.DefaultContextLines )]
        public uint NumberOfContextLines
        {
            get { return _numberOfContextLines; }
            set { _numberOfContextLines = value; }
        } private uint _numberOfContextLines = FileDiffer.DefaultContextLines;

        /// <summary>
        /// The maximum number of lines included from each file
        /// that's part of the changeset.
        /// </summary>
        [DefaultValue( FileDiffer.DefaultMaxIncludedLines )]
        public uint MaxFileLines
        {
            get { return _maxFileLines; }
            set { _maxFileLines = value; }
        } private uint _maxFileLines = FileDiffer.DefaultMaxIncludedLines;

        /// <summary>
        /// The maximum number of files that will be processed
        /// from a changeset.
        /// </summary>
        public uint MaxFiles
        {
            get { return _maxFiles; }
            set { _maxFiles = value; }
        } private uint _maxFiles = 20;

        /// <summary>
        /// The title of the changeset information, suitable for use
        /// as an e-mail subject.
        /// </summary>
        /// <remarks>
        /// This property is only valid after the changeset has been processed.
        /// </remarks>
        public string Title
        {
            private set
            {
                string  newTitle        = value;

                // Remove any line-break characters from the title.
                newTitle    = newTitle.Replace( '\r', ' ' );
                _title      = newTitle.Replace( '\n', ' ' );
            }
            get { return _title; }
        } private string _title;
        #endregion

        #region -- Protected properties
        /// <summary>
        /// The total number of lines that were added in this changeset.
        /// </summary>
        protected long TotalAddedLines
        {
            get { return _totalAddedLines; }
        }
        private long _totalAddedLines;

        /// <summary>
        /// The total number of lines that were deleted in this changeset.
        /// </summary>
        protected long TotalDeletedLines
        {
            get { return _totalDeletedLines; }
        }
        private long _totalDeletedLines;
        #endregion

        #region -- Private methods used to generate basic changeset information
        /// <summary>
        /// Adds the basic information about the changeset to the XML
        /// document being generated.
        /// </summary>
        /// <remarks>
        /// The basic information includes things such as the id of the changeset,
        /// who created the changeset, when it was created and the project the
        /// changeset is associated with.
        /// </remarks>
        /// <param name="xmlWriter">The <see cref="XmlTextWriter"/> object used to generate the changeset document.</param>
        private void AddBasicChangesetInformation( XmlTextWriter xmlWriter )
        {
            string projectName = GetProjectName();

            xmlWriter.WriteStartElement( "changeset" );

            _logger.Verbose( "Adding the basic changeset attributes" );
            xmlWriter.WriteAttributeString( "id", _changeset.ChangesetId.ToString( System.Globalization.CultureInfo.InvariantCulture ) );
            xmlWriter.WriteAttributeString( "committer", _changeset.Committer );
            xmlWriter.WriteAttributeString( "commitDate", _changeset.CreationDate.ToString() );
            xmlWriter.WriteAttributeString( "project", projectName );

            // Keep track of whether we have more files in the changeset than will be included in the document.
            if ( _changeset.Changes.Length > _maxFiles )
            {
                long numberOfAdditionalItems = _changeset.Changes.LongLength - _maxFiles;

                xmlWriter.WriteAttributeString( "itemsTruncated", numberOfAdditionalItems.ToString( System.Globalization.CultureInfo.InvariantCulture ) );
            }

            if ( string.IsNullOrEmpty( _changeset.Comment ) )
            {
                xmlWriter.WriteElementString( "comment", Properties.Strings.ChangesetDiff_NoComment );
            }
            else
            {
                xmlWriter.WriteElementString( "comment", _changeset.Comment );
            }

            _logger.Verbose( "Adding related work items" );
            AddRelatedWorkItems( xmlWriter );
            _logger.Verbose( "Adding policy overrides" );
            AddPolicyOverrides( xmlWriter );

            // Generate the title for this changeset analysis.
            SetTitle( projectName );

            xmlWriter.WriteEndElement();
            _logger.Verbose( "Done adding basic changeset information" );
        }

        /// <summary>
        /// Generates the title to use for this changeset analysis.
        /// </summary>
        /// <remarks>
        /// The title is accessible through the <see cref="Title"/>
        /// property.
        /// </remarks>
        /// <param name="projectName">The name of the TFS project the changeset belongs to.</param>
        private void SetTitle( string projectName )
        {
            string comment = string.IsNullOrEmpty( _changeset.Comment ) ? Properties.Strings.ChangesetDiff_NoComment : _changeset.Comment;
            string commentExtract;
            int newLinePosition = -1;

            _logger.Verbose( "Setting the title for the changeset analysis document" );

            if ( comment.Length > MaxCommentLength )
            {
                _logger.Verbose( "The length of the changeset comment is longer than the maximum length, abbreviating it" );
                commentExtract = string.Format( System.Globalization.CultureInfo.InvariantCulture, "{0}...", comment.Substring( 0, MaxCommentLength ) );
            }
            else
            {
                commentExtract = comment;
            }

            // We can't have newlines in a title, so truncate the title at the first newline.
            newLinePosition = commentExtract.IndexOf( Environment.NewLine, StringComparison.Ordinal );
            if ( newLinePosition != -1 )
            {
                _logger.Verbose( "Truncating the title at the first found newline in the comment" );
                commentExtract = commentExtract.Substring( 0, newLinePosition );
            }

            Title = string.Format( System.Globalization.CultureInfo.InvariantCulture, Properties.Strings.ChangesetDiff_Title, projectName, _changeset.ChangesetId, commentExtract );
            _logger.Verbose( "The title is set to '{0}'", Title );
        }

        /// <summary>
        /// Returns the name of the TFS project the changeset being
        /// processed is part of.
        /// </summary>
        /// <returns>The name of the TFS project the changeset being processed is part of.</returns>
        private string GetProjectName()
        {
            Microsoft.TeamFoundation.VersionControl.Client.TeamProject tfsProject = _versionControl.GetTfsProject( _changeset );

            if ( tfsProject == null )
            {
                _logger.Warning( "Couldn't get the name of the TFS project associated with the changeset" );
                return Properties.Strings.ChangesetDiff_UnknownProject;
            }
            else
            {
                _logger.Verbose( "The TFS project associated with the changeset is '{0}'", tfsProject.Name );
                return tfsProject.Name;
            }
        }
        #endregion

        #region -- Work item processing
        /// <summary>
        /// Adds any work items related to the changeset to the
        /// document.
        /// </summary>
        /// <param name="xmlWriter">The <see cref="XmlTextWriter"/> object used to generate the changeset document.</param>
        private void AddRelatedWorkItems( XmlTextWriter xmlWriter )
        {
            _logger.Verbose( "Adding {0} related work items to the document", _changeset.WorkItems.Length );

            foreach ( WorkItem relatedWorkItem in _changeset.WorkItems )
            {
                AddRelatedWorkItem( xmlWriter, relatedWorkItem );
            }

            _logger.Verbose( "All related work items processed" );
        }

        /// <summary>
        /// Adds an individual related work item to the document
        /// being generated.
        /// </summary>
        /// <param name="xmlWriter">The <see cref="XmlTextWriter"/> object used to generate the changeset document.</param>
        /// <param name="relatedWorkItem">A work item related to the changeset being processed.</param>
        private void AddRelatedWorkItem( XmlTextWriter xmlWriter, WorkItem relatedWorkItem )
        {
            _logger.Verbose( "Adding workitem #{0} [{1}] to the analysis document", relatedWorkItem.Id, relatedWorkItem.Title );
            xmlWriter.WriteStartElement( "workItem" );
            xmlWriter.WriteAttributeString( "type", relatedWorkItem.Type.Name );
            xmlWriter.WriteAttributeString( "id", relatedWorkItem.Id.ToString( System.Globalization.CultureInfo.InvariantCulture ) );
            xmlWriter.WriteAttributeString( "state", relatedWorkItem.State );
            xmlWriter.WriteAttributeString( "title", relatedWorkItem.Title );
            xmlWriter.WriteAttributeString( "assignedTo", relatedWorkItem.Fields[ CoreField.AssignedTo ].Value.ToString() );
            xmlWriter.WriteEndElement();
            _logger.Verbose( "Workitem #{0} added", relatedWorkItem.Id );
        }
        #endregion

        #region -- Policy override processing
        /// <summary>
        /// Adds information about any overridden policies to the changeset document
        /// being generated.
        /// </summary>
        /// <param name="xmlWriter">The <see cref="XmlTextWriter"/> object used to generate the changeset document.</param>
        private void AddPolicyOverrides( XmlTextWriter xmlWriter )
        {
            if ( _changeset.PolicyOverride != null && _changeset.PolicyOverride.PolicyFailures.Length > 0 )
            {
                _logger.Verbose( "Adding {0} policy failures to the document", _changeset.PolicyOverride.PolicyFailures.Length );

                xmlWriter.WriteStartElement( "policyOverride" );
                xmlWriter.WriteAttributeString( "comment", _changeset.PolicyOverride.Comment );

                foreach ( PolicyFailureInfo policyFailure in _changeset.PolicyOverride.PolicyFailures )
                {
                    AddPolicyFailure( xmlWriter, policyFailure );
                }

                xmlWriter.WriteEndElement();

                _logger.Verbose( "All policy failures processed" );
            }
        }

        /// <summary>
        /// Adds a policy failure to the changeset document being generated.
        /// </summary>
        /// <param name="xmlWriter">The <see cref="XmlTextWriter"/> object used to generate the changeset document.</param>
        /// <param name="policyFailure">The information for a check-in policy that failed.</param>
        private void AddPolicyFailure( XmlTextWriter xmlWriter, PolicyFailureInfo policyFailure )
        {
            _logger.Verbose( "Adding a policy failure: {0} - {1}", policyFailure.PolicyName, policyFailure.Message );
            xmlWriter.WriteStartElement( "policyFailure" );
            xmlWriter.WriteAttributeString( "policy", policyFailure.PolicyName );
            xmlWriter.WriteAttributeString( "message", policyFailure.Message );
            xmlWriter.WriteEndElement();
            _logger.Verbose( "Policy failure {0} added", policyFailure.PolicyName );
        }
        #endregion

        #region -- Private methods used to process the items that are part of the changeset
        /// <summary>
        /// Adds information about a changeset item to the XML document being generated.
        /// </summary>
        /// <param name="xmlWriter">The <see cref="XmlTextWriter"/> object used to generate the changeset document.</param>
        [SuppressMessage( "Microsoft.Performance", "CA1817", Justification = "We use a for loop instead of a foreach since we need to break after the maximum number of files configured have been processed." )]
        private void AddItemInformation( XmlTextWriter xmlWriter )
        {
            _logger.Information( "Processing {0} changes in the changeset", _changeset.Changes.Length );

            for( int changesetIndex = 0 ; changesetIndex < _changeset.Changes.Length && ((uint) changesetIndex ) < _maxFiles ; changesetIndex++ )
            {
                Change change = _changeset.Changes[ changesetIndex ];

                xmlWriter.WriteStartElement( "item" );
                xmlWriter.WriteAttributeString( "id", change.Item.ItemId.ToString( System.Globalization.CultureInfo.InvariantCulture ) );
                xmlWriter.WriteAttributeString( "change", change.ChangeType.ToString() );

                switch ( change.Item.ItemType )
                {
                    case ItemType.File:
                        _logger.Verbose( "Processing a file item - {0}", change.Item.ServerItem );
                        ProcessFileItem( xmlWriter, change );
                        break;

                    case ItemType.Folder:
                        _logger.Verbose( "Processing a folder item - {0}", change.Item.ServerItem );
                        AddFolderInformation( xmlWriter, change );
                        break;
                }

                xmlWriter.WriteEndElement();
            }

            // Write the total number of added and deleted lines to the document.
            xmlWriter.WriteStartElement( "itemTotals" );
            xmlWriter.WriteAttributeString( "totalAddedLines", TotalAddedLines.ToString( System.Globalization.CultureInfo.InvariantCulture ) );
            xmlWriter.WriteAttributeString( "totalDeletedLines", TotalDeletedLines.ToString( System.Globalization.CultureInfo.InvariantCulture ) );
            xmlWriter.WriteEndElement();

            _logger.Verbose( "All changes in the changeset processed" );
        }

        /// <summary>
        /// Adds basic information about a changeset item, such as the path
        /// and type of the item to the XML document being generated.
        /// </summary>
        /// <param name="writer">The <see cref="XmlTextWriter"/> object used to generate the changeset document.</param>
        /// <param name="change">The change information for the changeset item.</param>
        private void WriteItemAttributes( XmlWriter writer, Change change )
        {
            _logger.Verbose( "Writing basic item attributes: Path = '{0}', Type = '{0}'", change.Item.ServerItem, change.Item.ItemType );

            writer.WriteAttributeString( "path", change.Item.ServerItem );
            writer.WriteAttributeString( "type", change.Item.ItemType.ToString() );
        }

        /// <summary>
        /// Adds basic information about a file in the changeset along with the
        /// actual changes made to the file to the XML document being generated.
        /// </summary>
        /// <param name="writer">The <see cref="XmlTextWriter"/> object used to generate the changeset document.</param>
        /// <param name="change">The information about the changeset item.</param>
        /// <param name="differenceResult">The differences between the file in the changeset and the previous version of the file.</param>
        private void WriteItemAttributes( XmlWriter writer, Change change, FileDifferenceResult differenceResult )
        {
            int numberOfDeletedLines = 0;
            int numberOfInsertedLines = 0;

            // First add the basic information about the file...
            WriteItemAttributes( writer, change );

            // ...then add information about the number of lines changed in the file.
            _logger.Verbose( "Writing {0} file differences to the document for item '{1}'", differenceResult.Differences.Length, change.Item.ServerItem );
            foreach ( FileDifference difference in differenceResult.Differences )
            {
                numberOfDeletedLines += difference.NumberOfDeletedLines;
                numberOfInsertedLines += difference.NumberOfInsertedLines;
            }

            // Keep track of the total number of lines changed by the changeset.
            _totalAddedLines += numberOfInsertedLines;
            _totalDeletedLines += numberOfDeletedLines;

            writer.WriteAttributeString( "deletedLines", numberOfDeletedLines.ToString( System.Globalization.CultureInfo.InvariantCulture ) );
            writer.WriteAttributeString( "insertedLines", numberOfInsertedLines.ToString( System.Globalization.CultureInfo.InvariantCulture ) );
            writer.WriteAttributeString( "truncated", differenceResult.Truncated.ToString() );

            _logger.Verbose( "All detail information written for item '{0}'", change.Item.ServerItem );
        }
        #endregion

        #region -- Private methods used to process files
        /// <summary>
        /// Adds the relevant information for a file item that's part of the changeset
        /// being processed, this includes content difference information assuming the
        /// file is of a type that should have it's details included.
        /// </summary>
        /// <param name="xmlWriter">The <see cref="XmlTextWriter"/> object used to generate the changeset document.</param>
        /// <param name="change">The information about the file item that should be processed.</param>
        private void ProcessFileItem( XmlTextWriter xmlWriter, Change change )
        {
            if ( IncludeFileDetails( change.Item ) )
            {
                _logger.Verbose( "Including detailed information for item '{0}'", change.Item.ServerItem );

                if ( change.ChangeType == ChangeType.Delete )
                {
                    AddFileDeleteInformation( xmlWriter, change );
                }
                else if ( ( change.ChangeType & ChangeType.Add ) != 0 )
                {
                    AddFileAddInformation( xmlWriter, change );
                }
                else if ( ( change.ChangeType & ChangeType.Edit ) != 0 )
                {
                    AddFileEditInformation( xmlWriter, change );
                }
                else
                {
                    _logger.Verbose( "Including only basic information for item '{0}' due to change type ({1})", change.Item.ServerItem, change.ChangeType );
                    WriteItemAttributes( xmlWriter, change );
                }
            }
            else
            {
                // The file is of a type that shouldn't have detail information
                // included, just include the basic data about the file.
                _logger.Verbose( "Including only basic information for item '{0}'", change.Item.ServerItem );
                WriteItemAttributes( xmlWriter, change );
            }
        }

        /// <summary>
        /// Checks if the specified file item should have any detail information
        /// included in the XML document being generated.
        /// </summary>
        /// <param name="fileItem">A file item that's part of the changeset being processed.</param>
        /// <returns><c>true</c> if the file details should be included, <c>false</c> otherwise.</returns>
        private bool IncludeFileDetails( Item fileItem )
        {
            string fileExtension = Path.GetExtension( fileItem.ServerItem );

            return _fileDetailExtensions.ContainsKey( fileExtension );
        }

        /// <summary>
        /// Adds information about a file that was edited in the changeset
        /// to the XML document being generated.
        /// </summary>
        /// <param name="xmlWriter">The <see cref="XmlTextWriter"/> object used to generate the changeset document.</param>
        /// <param name="fileChange">The information about the edited file in the changeset.</param>
        private void AddFileEditInformation( XmlWriter xmlWriter, Change fileChange )
        {
            FileDifferenceResult differences = GetFileDifferences( fileChange.Item );

            if ( differences != null )
            {
                _logger.Verbose( "Adding file edit information for '{0}'", fileChange.Item.ServerItem );
                WriteItemAttributes( xmlWriter, fileChange, differences );
                WriteFileDifferences( xmlWriter, differences );
            }
            else
            {
                _logger.Warning( "Couldn't add the file edit information for '{0}'", fileChange.Item.ServerItem );
                WriteItemAttributes( xmlWriter, fileChange );
            }
        }

        /// <summary>
        /// Adds information about a file that was deleted in the changeset
        /// to the XML document being generated.
        /// </summary>
        /// <param name="xmlWriter">The <see cref="XmlTextWriter"/> object used to generate the changeset document.</param>
        /// <param name="fileChange">The information about the deleted file in the changeset.</param>
        private void AddFileDeleteInformation( XmlWriter xmlWriter, Change fileChange )
        {
            _logger.Verbose( "Adding file delete information for '{0}'", fileChange.Item.ServerItem );

            WriteItemAttributes( xmlWriter, fileChange );
            WriteFileDeleteLines( xmlWriter, fileChange.Item );
        }

        /// <summary>
        /// Adds information about a file that was added in the changeset to
        /// the XML document being generated.
        /// </summary>
        /// <param name="xmlWriter">The <see cref="XmlTextWriter"/> object used to generate the changeset document.</param>
        /// <param name="fileChange">The information about the added file in the changeset.</param>
        private void AddFileAddInformation( XmlWriter xmlWriter, Change fileChange )
        {
            _logger.Verbose( "Adding file add information for '{0}'", fileChange.Item.ServerItem );

            WriteItemAttributes( xmlWriter, fileChange );
            WriteFileAddLines( xmlWriter, fileChange.Item );
        }

        /// <summary>
        /// Retrieves the differences between the changeset version and the
        /// previous version of the specified file item.
        /// </summary>
        /// <param name="fileItem">A file item that was edited in the changeset.</param>
        /// <returns>
        /// An array of <see cref="FileDifference"/> objects representing the differences
        /// between the two versions of the file. If the versions are identical, the returned
        /// array contains 0 elements.
        /// </returns>
        private FileDifferenceResult GetFileDifferences( Item fileItem )
        {
            string newVersionFilePath = Path.GetTempFileName();
            string oldVersionFilePath = Path.GetTempFileName();

            _logger.Information( "Determining the differences between the current and previous version of '{0}'", fileItem.ServerItem );
            _logger.Verbose( "Current version path = '{0}', previous version path = '{1}'", newVersionFilePath, oldVersionFilePath );

            try
            {
                _logger.Verbose( "Downloading the current version of '{0}'", fileItem.ServerItem );
                _versionControl.DownloadFile( fileItem, newVersionFilePath );

                _logger.Verbose( "Downloading the previous version of '{0}'", fileItem.ServerItem );
                _versionControl.DownloadFile( fileItem.ItemId, fileItem.ChangesetId - 1, oldVersionFilePath );

                _logger.Verbose( "Comparing the current and previous version of '{0}'", fileItem.ServerItem );
                return CompareFiles( oldVersionFilePath, newVersionFilePath );
            }
            catch ( ItemDownloadException idex )
            {
                _logger.Error( "The item '{0}' (current version = {1}) couldn't be downloaded when changeset version {2} was requested", fileItem.ServerItem, fileItem.ChangesetId, idex.ChangesetId );
                return null;
            }
            finally
            {
                _logger.Verbose( "Deleting the downloaded current and previous version of '{0}'", fileItem.ServerItem );
                File.Delete( newVersionFilePath );
                File.Delete( oldVersionFilePath );
            }
        }

        /// <summary>
        /// Adds the information about the lines of text that make up the
        /// content of a file that was added in the changeset.
        /// </summary>
        /// <param name="xmlWriter">The <see cref="XmlTextWriter"/> object used to generate the changeset document.</param>
        /// <param name="fileItem">The file item that was added in the changeset.</param>
        private void WriteFileAddLines( XmlWriter xmlWriter, Item fileItem )
        {
            string addedFilePath = Path.GetTempFileName();

            _logger.Information( "Writing the content of the file added - {0}", fileItem.ServerItem );
            _logger.Verbose( "Added file download path = '{0}'", addedFilePath );

            try
            {
                string[] addedLines;

                _logger.Verbose( "Downloading the added file '{0}'", fileItem.ServerItem );
                _versionControl.DownloadFile( fileItem, addedFilePath );

                // TODO: This is memory intensive and could be replaced by just reading the lines one by one...
                _logger.Verbose( "Reading the contents of the added file '{0}'", fileItem.ServerItem );
                addedLines = File.ReadAllLines( addedFilePath );
                _totalAddedLines += addedLines.Length;

                _logger.Verbose( "Read {0} lines from '{1}'", addedLines.Length, fileItem.ServerItem );
                xmlWriter.WriteAttributeString( "insertedLines", addedLines.Length.ToString( System.Globalization.CultureInfo.InvariantCulture ) );

                xmlWriter.WriteStartElement( "difference" );
                _logger.Verbose( "Writing the added lines to the document for file '{0}'", fileItem.ServerItem );
                WriteTextLines(xmlWriter, LineType.Added, addedLines );
                xmlWriter.WriteEndElement();
            }
            finally 
            {
                _logger.Verbose( "Deleting the downloaded version of the added file '{0}'", fileItem.ServerItem );
                File.Delete( addedFilePath );
            }

            _logger.Information( "Successfully processed the added file '{0}'", fileItem.ServerItem );
        }

        /// <summary>
        /// Adds the information about the lines of text that made up
        /// the content of a file that was deleted in the changeset.
        /// </summary>
        /// <param name="xmlWriter">The <see cref="XmlTextWriter"/> object used to generate the changeset document.</param>
        /// <param name="fileItem">The file item that was deleted in the changeset.</param>
        private void WriteFileDeleteLines( XmlWriter xmlWriter, Item fileItem )
        {
            string deletedFilePath = Path.GetTempFileName();

            _logger.Information( "Writing the content of the file deleted - {0}", fileItem.ServerItem );
            _logger.Verbose( "Deleted file download path = '{0}'", deletedFilePath );

            try
            {
                string[] deletedLines;

                _logger.Verbose( "Downloading the deleted file '{0}'", fileItem.ServerItem );
                _versionControl.DownloadFile( fileItem.ItemId, fileItem.ChangesetId - 1, deletedFilePath );

                // TODO: This is memory intensive, we could just count the number of lines one by one to save memory...
                _logger.Verbose( "Reading the contents of the deleted file '{0}'", fileItem.ServerItem );
                deletedLines = File.ReadAllLines( deletedFilePath );
                _totalDeletedLines += deletedLines.Length;

                _logger.Verbose( "{0} lines in the deleted file '{1}'", deletedLines.Length, fileItem.ServerItem );
                xmlWriter.WriteAttributeString( "deletedLines", deletedLines.Length.ToString( System.Globalization.CultureInfo.InvariantCulture ) );

                xmlWriter.WriteStartElement( "difference" );
                WriteTextLines(xmlWriter, LineType.Deleted, deletedLines );
                xmlWriter.WriteEndElement();
            }
            catch ( ItemDownloadException idex )
            {
                _logger.Error( "The deleted item '{0}' (current version = {1}) couldn't be downloaded when changeset version {2} was requested", fileItem.ServerItem, fileItem.ChangesetId, idex.ChangesetId );
            }
            finally
            {
                _logger.Verbose( "Deleting the downloaded deleted file '{0}'", fileItem.ServerItem );
                File.Delete( deletedFilePath );
            }

            _logger.Information( "Successfully processed the deleted file '{0}'", fileItem.ServerItem );
        }

        /// <summary>
        /// Compares two versions of a file and generates the information
        /// about the differences between the two versions of the file.
        /// </summary>
        /// <remarks>
        /// This method also sets the <c>_currentFileTruncated</c> member variable
        /// if the file contains too many lines to show add all lines to the XML
        /// document being generated.
        /// </remarks>
        /// <param name="oldVersion">The fully qualified path of the old (previous) version of the file.</param>
        /// <param name="newVersion">The fully qualified path of the new (changeset) version of the file.</param>
        /// <returns>
        /// An array of <see cref="FileDifference"/> objects detailing the differences
        /// between the two versions of the file.
        /// </returns>
        private FileDifferenceResult CompareFiles( string oldVersion, string newVersion )
        {
            FileDiffer differ = new FileDiffer( oldVersion, newVersion, _maxFileLines, _numberOfContextLines, _highlightPrefixes );

            _logger.Verbose( "Comparing two versions of a file (old = '{0}', new = '{1}')", oldVersion, newVersion );
            return differ.GetFileDifferences();
        }

        /// <summary>
        /// Adds the differences between the changeset version of the file and the previous
        /// version of the file to the XML document being generated.
        /// </summary>
        /// <param name="xmlWriter">The <see cref="XmlTextWriter"/> object used to generate the changeset document.</param>
        /// <param name="differenceResult">The information about the changes made to the changeset file.</param>
        private void WriteFileDifferences( XmlWriter xmlWriter, FileDifferenceResult differenceResult )
        {
            _logger.Verbose( "Writing {0} found differences to the XML document", differenceResult.Differences.Length );
            _logger.Verbose( "Truncated = {0}", differenceResult.Truncated );

            foreach ( FileDifference difference in differenceResult.Differences )
            {
                xmlWriter.WriteStartElement( "difference" );
                xmlWriter.WriteAttributeString( "summary", difference.ToString() );

                WriteTextLines(xmlWriter, difference.Lines );

                xmlWriter.WriteEndElement();
            }
        }

        /// <summary>
        /// Writes a number of lines from a file to the XML document being generated.
        /// </summary>
        /// <param name="xmlWriter">The <see cref="XmlTextWriter"/> object used to generate the changeset document.</param>
        /// <param name="lines">The lines of text that should be added to the XML document.</param>
        private void WriteTextLines( XmlWriter xmlWriter, ReadOnlyCollection<DifferenceLine> lines )
        {
            _logger.Verbose( "Writing {0} lines of text.", lines.Count );

            for( int lineIndex = 0 ; lineIndex < lines.Count && lineIndex <= _maxFileLines ; lineIndex++ )
            {
                DifferenceLine currentLine = lines[ lineIndex ];

                xmlWriter.WriteStartElement( "line" );
                xmlWriter.WriteAttributeString( "type", currentLine.LineType.ToString() );
                xmlWriter.WriteAttributeString( "source", currentLine.LineSource.ToString() );
                xmlWriter.WriteAttributeString( "lineNumber", currentLine.LineNumber.ToString( System.Globalization.CultureInfo.InvariantCulture ) );

                // Add any code highlight information we may have.
                if ( currentLine.HasHighlight )
                {
                    xmlWriter.WriteAttributeString( "highlightText", currentLine.HighlightText );
                }

                WriteTextLine( xmlWriter, currentLine.LineText );
                xmlWriter.WriteEndElement();
            }
        }
        /// <summary>
        /// Writes a number of lines from a file to the XML document being generated.
        /// </summary>
        /// <param name="xmlWriter">The <see cref="XmlTextWriter"/> object used to generate the changeset document.</param>
        /// <param name="lineType">The type of line that should be written.</param>
        /// <param name="lines">The lines of text that should be added to the XML document.</param>
        private void WriteTextLines( XmlWriter xmlWriter, LineType lineType, string[] lines )
        {
            string type = lineType.ToString();

            _logger.Verbose( "Writing {0} lines of text (type {1})", lines.Length, type );

            for ( int lineIndex = 0 ; lineIndex < lines.Length && lineIndex <= _maxFileLines ; lineIndex++ )
            {
                xmlWriter.WriteStartElement( "line" );
                xmlWriter.WriteAttributeString( "type", type );
                WriteTextLine( xmlWriter, lines[ lineIndex ] );
                xmlWriter.WriteEndElement();
            }
        }

        /// <summary>
        /// Writes a line of text to the XML document as a CDATA section, properly handling the
        /// case where the text to be written contains one or more CDATA section terminators.
        /// </summary>
        /// <param name="xmlWriter">The <see cref="XmlTextWriter"/> object used to generate the changeset document.</param>
        /// <param name="textLine">The line of text to write to the XML document.</param>
        private static void WriteTextLine( XmlWriter xmlWriter, string textLine )
        {
            if ( textLine.IndexOf( CDataTerminator, StringComparison.OrdinalIgnoreCase ) == -1 )
            {
                // The line of text to write doesn't contain any CDATA terminator, just write it in a single CDATA section.
                xmlWriter.WriteCData( textLine );
            }
            else
            {
                int previousTerminator = 0;
                int terminatorIndex = textLine.IndexOf( CDataTerminator, StringComparison.OrdinalIgnoreCase );

                // We have at least one CDATA section terminator in the line of text to write. Split
                // the line of text into multiple CDATA sections in the XML document.
                while ( terminatorIndex != -1 )
                {
                    int currentSegmentLength = terminatorIndex + 2 - previousTerminator;
                    string currentSegment = textLine.Substring( previousTerminator, currentSegmentLength );

                    // Write the section of the line between the previous terminator
                    // and the current terminator, including the ']]' portion of the
                    // current terminator.
                    xmlWriter.WriteCData( currentSegment );

                    // Adjust the position of the now previous terminator so that it
                    // starts at the '>' portion of the terminator to ensure that gets
                    // included in the next section of the line written to the document.
                    previousTerminator = terminatorIndex + 2;
                    terminatorIndex = textLine.IndexOf( CDataTerminator, previousTerminator, StringComparison.OrdinalIgnoreCase );
                }

                if ( previousTerminator > 0 )
                {
                    // Write the last part of the line to the document as its own CDATA section.
                    xmlWriter.WriteCData( textLine.Substring( previousTerminator ) );
                }
            }
        }
        #endregion

        #region -- Private methods used to process folders
        /// <summary>
        /// Adds basic information about a folder item in the changeset
        /// to the XML document being generated.
        /// </summary>
        /// <param name="xmlWriter">The <see cref="XmlTextWriter"/> object used to generate the changeset document.</param>
        /// <param name="folderChange">The information about the changed folder item in the changeset.</param>
        private void AddFolderInformation( XmlWriter xmlWriter, Change folderChange )
        {
            WriteItemAttributes( xmlWriter, folderChange );
        }
        #endregion
    }
}
